Skip to content

feat: 계획 관리 페이지 UI 구현#39

Merged
GamjaIsMine02 merged 6 commits into
devfrom
feature/plan-management-page
Jan 19, 2026
Merged

feat: 계획 관리 페이지 UI 구현#39
GamjaIsMine02 merged 6 commits into
devfrom
feature/plan-management-page

Conversation

@GamjaIsMine02
Copy link
Copy Markdown
Contributor

@GamjaIsMine02 GamjaIsMine02 commented Jan 19, 2026

1) 작업한 이슈번호

#38

2) 변경 요약 (What & Why)

  • 무엇을 변경했는지: 계획 관리 페이지 UI를 구현
  • 변경했는지(문제/목표):

3) 스크린샷/동영상 (UI 변경 시)

전/후 비교, 반응형(모바일/데스크톱) 캡쳐

  • Before:
  • After:
image

4) 상세 변경사항

  • 라우팅/페이지: app/(with-sidebar)/plans/page.tsx
  • 컴포넌트: components/common/PageHeader.tsx, components/common/Sidebar.tsx, components/home/Card.tsx, components/plans/AddPlanButton.tsx, components/plans/PlanSection.tsx, components/plans/SearchBar.tsx, components/plans/TaskItem.tsx
  • 상태관리:
  • API 호출:
  • 스타일:
  • 기타:

5) 참고사항

  • 기능 구현 후 홈 페이지의 기능과 연결할 예정

Summary by CodeRabbit

릴리스 노트

  • New Features

    • 계획 관리 페이지 추가: 새로운 계획을 만들고 관리할 수 있는 전용 페이지 생성
    • 계획 검색 기능: 계획 목록에 검색 바 추가
    • 작업 추적: 계획 내 작업 항목을 생성하고 완료 상태 관리 가능
    • 통계 대시보드: 계획 관리 페이지에 통계 카드 표시
  • Style

    • 카드 컴포넌트 시각화 개선

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Jan 19, 2026

📝 Walkthrough

Walkthrough

플랜 관리 기능을 위한 새로운 페이지 및 관련 UI 컴포넌트들을 추가합니다. PageHeader, AddPlanButton, PlanSection, SearchBar, TaskItem 컴포넌트를 새로 작성하고, 기존 Sidebar와 Card 컴포넌트를 일부 수정합니다.

Changes

응집도 / 파일 변경 내용
플랜 관리 페이지
app/(with-sidebar)/plans/page.tsx
플랜 관리 페이지 신규 추가: PageHeader, 통계 카드 그리드, 검색 바, PlanSection, AddPlanButton을 조합한 레이아웃
공통 컴포넌트
components/common/PageHeader.tsx, components/common/Sidebar.tsx
PageHeader 신규 추가(제목, 강조 텍스트, 설명 렌더링); Sidebar 네비게이션 라벨 "새 계획 만들기" → "계획 관리" 변경 및 경로 업데이트
플랜 관련 컴포넌트
components/plans/AddPlanButton.tsx, components/plans/PlanSection.tsx, components/plans/SearchBar.tsx, components/plans/TaskItem.tsx
플랜 추가 버튼, 축소 가능한 플랜 섹션(진행률 표시, 작업 목록), 검색 입력 필드, 완료 상태 시각화 작업 아이템 신규 추가
UI 스타일 조정
components/home/Card.tsx
Card 컴포넌트에서 border 클래스 제거

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Poem

🐰 새 계획의 흐름을 짜며
검색 창에 담긴 꿈들
미완의 할 일들을 순서대로
단계별 섹션에 보관해두고
초록 버튼으로 또 다른 시작을 🚀

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed PR 제목은 계획 관리 페이지 UI 구현이라는 주요 변경사항을 명확하고 간결하게 요약하고 있습니다.
Description check ✅ Passed PR 설명은 템플릿의 대부분 섹션을 포함하고 있으나, '왜 변경했는지'와 '전/후 비교' 부분이 미흡합니다.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@GamjaIsMine02
Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Jan 19, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🤖 Fix all issues with AI agents
In `@components/common/Sidebar.tsx`:
- Line 18: Update the Sidebar menu entry for "계획 관리" so its href points to the
existing plans listing route instead of a non-existent new-plan route: locate
the menu item { label: '계획 관리', href: '/plans/new', icon: CalendarPlus } in the
Sidebar component and change href to '/plans' so navigation goes to the plans
list (where AddPlanButton handles creation).

In `@components/plans/PlanSection.tsx`:
- Around line 36-55: The header toggle currently uses a clickable div wrapping a
nested button which breaks semantics and keyboard access; replace the outer div
(the element with onClick={() => setIsOpen(!isOpen)} containing title,
description, and the right-side counts/icon) with a single interactive element
(use a button) and move the click handler to that button (calling setIsOpen).
Keep the chevron icons (ChevronUp/ChevronDown) as non-interactive decorative
elements (e.g., span) and ensure the button exposes aria-expanded tied to isOpen
and preserves the existing layout and text elements (title, description,
completedCount/totalCount). Ensure no nested interactive controls remain.
- Around line 74-75: The Add-subitem button in the PlanSection component can
unintentionally submit parent forms because it lacks an explicit type; update
the JSX for the button that renders "새 하위항목 추가" inside PlanSection to include
type="button" (keeping existing className and children intact) so it won't act
as a submit control when nested in a form.

In `@components/plans/TaskItem.tsx`:
- Line 1: 파일 상단 주석이 잘못된 경로(`// components/dashboard/TaskItem.tsx`)로 되어 있어 혼동을
유발합니다; 수정하려면 파일의 최상단 주석을 실제 경로인 `// components/plans/TaskItem.tsx`로 변경하거나 불필요하면
주석을 제거하세요—수정 대상은 TaskItem 컴포넌트 파일의 파일-header 주석입니다.
- Around line 4-26: The checkbox-looking element in TaskItem is visually
interactive but has no accessibility or toggle behavior; update TaskItem to
accept an optional onToggle prop on TaskItemProps and replace or enhance the
checkbox div (the element rendering the Check icon when isCompleted is true) to
be an interactive control: use a semantic interactive element (e.g., button) or
add role="checkbox", aria-checked based on isCompleted, tabIndex, and keyboard
handling (toggle on Space/Enter) that calls onToggle, and only show
cursor-pointer when onToggle is provided; also include an accessible label
(aria-label or aria-labelledby) for the control so screen readers can announce
it.
🧹 Nitpick comments (5)
components/plans/SearchBar.tsx (1)

1-1: 파일 경로 주석이 실제 위치와 불일치

주석에는 components/dashboard/SearchBar.tsx로 되어 있지만, 실제 파일 경로는 components/plans/SearchBar.tsx입니다.

🔧 수정 제안
-// components/dashboard/SearchBar.tsx
+// components/plans/SearchBar.tsx
components/plans/AddPlanButton.tsx (2)

1-1: 파일 경로 주석이 실제 위치와 불일치

주석에는 components/dashboard/AddPlanButton.tsx로 되어 있지만, 실제 파일 경로는 components/plans/AddPlanButton.tsx입니다.

🔧 수정 제안
-// components/dashboard/AddPlanButton.tsx
+// components/plans/AddPlanButton.tsx

6-11: 버튼에 type="button" 명시 권장

<button> 요소는 type 속성이 없으면 폼 내에서 기본값이 "submit"이 됩니다. 의도치 않은 폼 제출을 방지하기 위해 명시적으로 type="button"을 추가하는 것이 좋습니다.

🔧 수정 제안
-    <button className="flex w-full flex-col items-center justify-center gap-2 rounded-2xl border-2 border-dashed border-gray-300 py-6 font-medium text-gray-500 transition-all hover:border-gray-400 hover:bg-gray-50">
+    <button type="button" className="flex w-full flex-col items-center justify-center gap-2 rounded-2xl border-2 border-dashed border-gray-300 py-6 font-medium text-gray-500 transition-all hover:border-gray-400 hover:bg-gray-50">
app/(with-sidebar)/plans/page.tsx (2)

1-1: 사용되지 않는 import 제거

FormField가 import 되어 있지만 컴포넌트 내에서 사용되지 않습니다.

🔧 수정 제안
-import FormField from '@/components/auth/FormField';
 import PageHeader from '@/components/common/PageHeader';

43-84: 하드코딩된 색상값 대신 테마 변수 사용 고려

통계 카드에서 #D5DCFB, #4757D3, #EBDBFC 등 하드코딩된 색상값을 사용하고 있습니다. 유지보수성과 디자인 일관성을 위해 Tailwind 테마 변수로 정의하는 것을 권장합니다.

현재 UI 구현 단계에서는 문제없지만, 향후 테마 시스템 구축 시 리팩토링을 고려해 주세요.

{ label: '새 계획 만들기', href: '/plans/new', icon: CalendarPlus },
{ label: '새 페이지 만들기', href: '/write', icon: CopyPlus },
];
{ label: '계획 관리', href: '/plans/new', icon: CalendarPlus },
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find all plans-related page files
find . -path "*/plans/*" -name "*.tsx" -o -path "*/plans/*" -name "*.ts" | grep -E "(page|layout)" | head -20

Repository: DeveloperBlog-Devflow/devflow

Length of output: 108


🏁 Script executed:

#!/bin/bash
# Check the Sidebar.tsx file around line 18 for context
cat -n components/common/Sidebar.tsx | sed -n '1,40p'

Repository: DeveloperBlog-Devflow/devflow

Length of output: 1367


🏁 Script executed:

#!/bin/bash
# Search for all references to '/plans' routes in the codebase
rg "plans" --type tsx --type ts -i | grep -E "(href|route|path)" | head -30

Repository: DeveloperBlog-Devflow/devflow

Length of output: 100


🏁 Script executed:

#!/bin/bash
# Check the plans directory structure
ls -la app/\(with-sidebar\)/plans/

Repository: DeveloperBlog-Devflow/devflow

Length of output: 241


🏁 Script executed:

#!/bin/bash
# Read the plans page file
cat -n app/\(with-sidebar\)/plans/page.tsx

Repository: DeveloperBlog-Devflow/devflow

Length of output: 4125


🏁 Script executed:

#!/bin/bash
# Search for all route directories under app
find app -type d -name "plans" -o -type d -name "*new*" | head -20

Repository: DeveloperBlog-Devflow/devflow

Length of output: 97


href 경로 수정 필요

"/plans/new" 경로가 존재하지 않습니다. 계획 관리 페이지는 /plans 경로에만 존재하며, 여기서 AddPlanButton을 통해 새 계획 생성을 처리합니다.

href를 /plans로 변경하세요.

🤖 Prompt for AI Agents
In `@components/common/Sidebar.tsx` at line 18, Update the Sidebar menu entry for
"계획 관리" so its href points to the existing plans listing route instead of a
non-existent new-plan route: locate the menu item { label: '계획 관리', href:
'/plans/new', icon: CalendarPlus } in the Sidebar component and change href to
'/plans' so navigation goes to the plans list (where AddPlanButton handles
creation).

Comment on lines +36 to +55
<div
className="flex cursor-pointer items-start justify-between"
onClick={() => setIsOpen(!isOpen)}
>
<div>
<h3 className="text-xl font-bold text-gray-900">{title}</h3>
<p className="mt-1 text-sm text-gray-500">{description}</p>
</div>

<div className="flex items-center gap-4">
<div className="text-right">
<span className="block text-xs text-gray-500">진행률</span>
<span className="text-lg font-bold text-[#556BD6]">
{completedCount}/{totalCount}
</span>
</div>
<button className="text-gray-400 hover:text-gray-600">
{isOpen ? <ChevronUp size={24} /> : <ChevronDown size={24} />}
</button>
</div>
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

토글 헤더가 비접근성(div 클릭)이며 중첩 버튼 구조입니다.
키보드 조작이 불가하고, 버튼이 중첩되어 시멘틱/접근성 문제가 있습니다. 토글 영역을 단일 button으로 바꾸고 아이콘은 비인터랙티브 요소로 두는 편이 안전합니다.

🛠️ 제안 수정
-      <div
-        className="flex cursor-pointer items-start justify-between"
-        onClick={() => setIsOpen(!isOpen)}
-      >
+      <button
+        type="button"
+        className="flex w-full items-start justify-between text-left"
+        onClick={() => setIsOpen((prev) => !prev)}
+        aria-expanded={isOpen}
+      >
         <div>
           <h3 className="text-xl font-bold text-gray-900">{title}</h3>
           <p className="mt-1 text-sm text-gray-500">{description}</p>
         </div>
 
         <div className="flex items-center gap-4">
           <div className="text-right">
             <span className="block text-xs text-gray-500">진행률</span>
             <span className="text-lg font-bold text-[`#556BD6`]">
               {completedCount}/{totalCount}
             </span>
           </div>
-          <button className="text-gray-400 hover:text-gray-600">
+          <span className="text-gray-400 hover:text-gray-600" aria-hidden="true">
             {isOpen ? <ChevronUp size={24} /> : <ChevronDown size={24} />}
-          </button>
+          </span>
         </div>
-      </div>
+      </button>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<div
className="flex cursor-pointer items-start justify-between"
onClick={() => setIsOpen(!isOpen)}
>
<div>
<h3 className="text-xl font-bold text-gray-900">{title}</h3>
<p className="mt-1 text-sm text-gray-500">{description}</p>
</div>
<div className="flex items-center gap-4">
<div className="text-right">
<span className="block text-xs text-gray-500">진행률</span>
<span className="text-lg font-bold text-[#556BD6]">
{completedCount}/{totalCount}
</span>
</div>
<button className="text-gray-400 hover:text-gray-600">
{isOpen ? <ChevronUp size={24} /> : <ChevronDown size={24} />}
</button>
</div>
<button
type="button"
className="flex w-full items-start justify-between text-left"
onClick={() => setIsOpen((prev) => !prev)}
aria-expanded={isOpen}
>
<div>
<h3 className="text-xl font-bold text-gray-900">{title}</h3>
<p className="mt-1 text-sm text-gray-500">{description}</p>
</div>
<div className="flex items-center gap-4">
<div className="text-right">
<span className="block text-xs text-gray-500">진행률</span>
<span className="text-lg font-bold text-[`#556BD6`]">
{completedCount}/{totalCount}
</span>
</div>
<span className="text-gray-400 hover:text-gray-600" aria-hidden="true">
{isOpen ? <ChevronUp size={24} /> : <ChevronDown size={24} />}
</span>
</div>
</button>
🤖 Prompt for AI Agents
In `@components/plans/PlanSection.tsx` around lines 36 - 55, The header toggle
currently uses a clickable div wrapping a nested button which breaks semantics
and keyboard access; replace the outer div (the element with onClick={() =>
setIsOpen(!isOpen)} containing title, description, and the right-side
counts/icon) with a single interactive element (use a button) and move the click
handler to that button (calling setIsOpen). Keep the chevron icons
(ChevronUp/ChevronDown) as non-interactive decorative elements (e.g., span) and
ensure the button exposes aria-expanded tied to isOpen and preserves the
existing layout and text elements (title, description,
completedCount/totalCount). Ensure no nested interactive controls remain.

Comment on lines +74 to +75
<button className="mt-2 flex w-full items-center justify-center gap-1 rounded-xl border-2 border-dashed border-[#556BD6]/30 py-3 text-sm font-medium text-[#556BD6] transition-colors hover:bg-[#556BD6]/5">
<Plus size={16} /> 새 하위항목 추가
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

버튼에 type 지정이 필요합니다.
폼 내부에 포함될 경우 의도치 않은 submit이 발생할 수 있습니다.

🛠️ 제안 수정
-          <button className="mt-2 flex w-full items-center justify-center gap-1 rounded-xl border-2 border-dashed border-[`#556BD6`]/30 py-3 text-sm font-medium text-[`#556BD6`] transition-colors hover:bg-[`#556BD6`]/5">
+          <button type="button" className="mt-2 flex w-full items-center justify-center gap-1 rounded-xl border-2 border-dashed border-[`#556BD6`]/30 py-3 text-sm font-medium text-[`#556BD6`] transition-colors hover:bg-[`#556BD6`]/5">
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<button className="mt-2 flex w-full items-center justify-center gap-1 rounded-xl border-2 border-dashed border-[#556BD6]/30 py-3 text-sm font-medium text-[#556BD6] transition-colors hover:bg-[#556BD6]/5">
<Plus size={16} /> 하위항목 추가
<button type="button" className="mt-2 flex w-full items-center justify-center gap-1 rounded-xl border-2 border-dashed border-[`#556BD6`]/30 py-3 text-sm font-medium text-[`#556BD6`] transition-colors hover:bg-[`#556BD6`]/5">
<Plus size={16} /> 하위항목 추가
🤖 Prompt for AI Agents
In `@components/plans/PlanSection.tsx` around lines 74 - 75, The Add-subitem
button in the PlanSection component can unintentionally submit parent forms
because it lacks an explicit type; update the JSX for the button that renders "새
하위항목 추가" inside PlanSection to include type="button" (keeping existing className
and children intact) so it won't act as a submit control when nested in a form.

@@ -0,0 +1,38 @@
// components/dashboard/TaskItem.tsx
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

파일 경로 주석이 실제 위치와 불일치합니다.
현재 주석이 dashboard로 되어 있어 파일 탐색/리뷰 시 혼선을 줄 수 있습니다.

🛠️ 제안 수정
-// components/dashboard/TaskItem.tsx
+// components/plans/TaskItem.tsx
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// components/dashboard/TaskItem.tsx
// components/plans/TaskItem.tsx
🤖 Prompt for AI Agents
In `@components/plans/TaskItem.tsx` at line 1, 파일 상단 주석이 잘못된 경로(`//
components/dashboard/TaskItem.tsx`)로 되어 있어 혼동을 유발합니다; 수정하려면 파일의 최상단 주석을 실제 경로인
`// components/plans/TaskItem.tsx`로 변경하거나 불필요하면 주석을 제거하세요—수정 대상은 TaskItem 컴포넌트
파일의 파일-header 주석입니다.

Comment on lines +4 to +26
interface TaskItemProps {
text: string;
date: string;
isCompleted: boolean;
}

export default function TaskItem({ text, date, isCompleted }: TaskItemProps) {
return (
<div
className={`mb-2 flex items-center gap-3 rounded-xl p-4 transition-colors ${
isCompleted ? 'bg-gray-50' : 'bg-gray-100'
}`}
>
{/* 체크박스 커스텀 */}
<div
className={`flex h-6 w-6 cursor-pointer items-center justify-center rounded-md border transition-colors ${
isCompleted
? 'border-green-500 bg-green-500 text-white'
: 'border-gray-300 bg-white hover:border-purple-400'
}`}
>
{isCompleted && <Check size={16} strokeWidth={3} />}
</div>
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

체크박스가 클릭 가능해 보이지만 동작·접근성이 없습니다.
UI만 구현하는 단계라면 클릭 affordance를 제거하거나, 실제 토글 핸들러/ARIA를 붙여 접근성을 확보해 주세요.

🛠️ 한 가지 접근: 토글 핸들러 옵션 추가 + 버튼化
 interface TaskItemProps {
   text: string;
   date: string;
   isCompleted: boolean;
+  onToggle?: () => void;
 }
 
-export default function TaskItem({ text, date, isCompleted }: TaskItemProps) {
+export default function TaskItem({ text, date, isCompleted, onToggle }: TaskItemProps) {
+  const isInteractive = typeof onToggle === 'function';
   return (
     <div
       className={`mb-2 flex items-center gap-3 rounded-xl p-4 transition-colors ${
         isCompleted ? 'bg-gray-50' : 'bg-gray-100'
       }`}
     >
       {/* 체크박스 커스텀 */}
-      <div
-        className={`flex h-6 w-6 cursor-pointer items-center justify-center rounded-md border transition-colors ${
-          isCompleted
-            ? 'border-green-500 bg-green-500 text-white'
-            : 'border-gray-300 bg-white hover:border-purple-400'
-        }`}
-      >
+      <button
+        type="button"
+        role="checkbox"
+        aria-checked={isCompleted}
+        onClick={onToggle}
+        disabled={!isInteractive}
+        className={`flex h-6 w-6 items-center justify-center rounded-md border transition-colors disabled:opacity-100 disabled:cursor-default ${
+          isInteractive ? 'cursor-pointer' : 'cursor-default'
+        } ${
+          isCompleted
+            ? 'border-green-500 bg-green-500 text-white'
+            : 'border-gray-300 bg-white'
+        } ${isInteractive ? 'hover:border-purple-400' : ''}`}
+      >
         {isCompleted && <Check size={16} strokeWidth={3} />}
-      </div>
+      </button>
🤖 Prompt for AI Agents
In `@components/plans/TaskItem.tsx` around lines 4 - 26, The checkbox-looking
element in TaskItem is visually interactive but has no accessibility or toggle
behavior; update TaskItem to accept an optional onToggle prop on TaskItemProps
and replace or enhance the checkbox div (the element rendering the Check icon
when isCompleted is true) to be an interactive control: use a semantic
interactive element (e.g., button) or add role="checkbox", aria-checked based on
isCompleted, tabIndex, and keyboard handling (toggle on Space/Enter) that calls
onToggle, and only show cursor-pointer when onToggle is provided; also include
an accessible label (aria-label or aria-labelledby) for the control so screen
readers can announce it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant